package com.lpj.base.id.generator.service.impl.provider;

import com.lpj.base.id.generator.service.impl.bean.IdMeta;
import com.lpj.base.id.generator.service.impl.bean.MachineIdEntity;
import com.lpj.base.id.generator.util.IpUtils;
import com.lpj.base.id.generator.util.TimeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Timer;
import java.util.TimerTask;

/**
 * 通过数据库动态分配机器id
 */
public class DbMachineIdProvider extends TimerTask implements MachineIdProvider {
    private static final Logger log = LoggerFactory
            .getLogger(DbMachineIdProvider.class);
    private static DbMachineIdProvider instance;
    private final JdbcTemplate jdbcTemplate;
    private final long LOCK_EXPIRE = 3; //秒
    private volatile long machineId;

    private DbMachineIdProvider(JdbcTemplate jdbcTemplate) {
        if (instance != null)
            throw new RuntimeException("Cannot instantiate more than once");
        log.debug("IpConfigurableMachineIdProvider constructed.");
        this.jdbcTemplate = jdbcTemplate;
        creatTableIfNotExists();
        Timer timer = new Timer();
        timer.schedule(this, MODIFIED_INTERVAL, MODIFIED_INTERVAL);
    }

    public static DbMachineIdProvider getInstance(JdbcTemplate jdbcTemplate) {
        if (instance == null) {
            synchronized (DbMachineIdProvider.class) {
                if (instance == null) {
                    instance = new DbMachineIdProvider(jdbcTemplate);
                }
            }
        }
        return instance;
    }

    @Override
    public long getMachineId(IdMeta idMeta) {
        boolean lock = lock();
        long currentTimeMillis = System.currentTimeMillis();
        if (lock) {
            try {
                for (long i = 1; i <= idMeta.getMaxMachineId(); i++) {
                    MachineIdEntity machineIdEntity = getMachineIdEntity(i);
                    if (machineIdEntity == null) {
                        insertMachineIdEntity(i, Integer.valueOf(idMeta.getExtTimeType()));
                        this.machineId = i;
                        log.info(String.format("MachineId %s obtained successfully", this.machineId));
                        return i;
                    } else if ((machineIdEntity.getLastTimestamp() != 0 && TimeUtils.getTime(currentTimeMillis) > TimeUtils.getTimestamp(machineIdEntity.getLastTimestamp(), machineIdEntity.getType()))
                            || (getDataBaseTime().getTime() - machineIdEntity.getModifiedOn() > MODIFIED_INTERVAL_CACHE
                            && currentTimeMillis - machineIdEntity.getUserTimestamp() > MODIFIED_INTERVAL_CACHE)) {
                        updateMachineIdEntity(i, Integer.valueOf(idMeta.getExtTimeType()));
                        this.machineId = i;
                        log.info(String.format("MachineId %s obtained successfully", this.machineId));
                        return i;
                    }
                }
            } finally {
                if (System.currentTimeMillis() / 1000 > currentTimeMillis / 1000 + LOCK_EXPIRE)
                    throw new RuntimeException("Timeout holding lock");
                unlock();
            }
        } else {
            throw new RuntimeException("cannot obtain the database global lock.");
        }
        throw new RuntimeException("System time called back or service reboot multiple times, resulting in no machine ID available");
    }

    @Override
    public void setBadMachineId(long machineId, long lastTimestamp) {
        log.warn(String.format("machineId %s used service time is called back, lastTimestamp: %s, currentTimestamp: %s", machineId, lastTimestamp, TimeUtils.getTime(System.currentTimeMillis())));
        jdbcTemplate.update("update DB_MACHINE_ID_PROVIDER set modified_on = ?,user_timestamp = ?,last_timestamp = ? where id = ?", new Object[]{getDataBaseTime(), System.currentTimeMillis(), lastTimestamp, machineId});
    }

    @Override
    public void run() {
        //对db进行心跳
        log.info(String.format("heartbeat -> machineId %s is in use", this.machineId));
        updateMachineIdEntity(this.machineId, null);
    }

    private Timestamp getDataBaseTime() {
        return jdbcTemplate.queryForObject("select now()", Timestamp.class);
    }

    private void creatTableIfNotExists() {
        jdbcTemplate.execute("create table if not exists `DB_MACHINE_ID_PROVIDER`(\n" +
                "  `id` int unsigned not null COMMENT '机器id',\n" +
                "  `type` int unsigned not null comment '时间位数类型 0 表示精确到秒  1表示精确到毫秒',\n" +
                "  `modified_on` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',\n" +
                "  `last_timestamp` bigint unsigned not null default 0 comment '上一次失败的时间戳',\n" +
                "  `user_timestamp` bigint unsigned not null comment '第一次使用的时间戳',\n" +
                "  `ip` varchar(50) null default null comment '被分配的机器ip',\n" +
                "  PRIMARY KEY (`id`)\n" +
                ")\n" +
                "COMMENT='id生成器机器id记录'\n" +
                "COLLATE='utf8_general_ci'\n" +
                "ENGINE=InnoDB\n" +
                ";");
        jdbcTemplate.execute("create table if not exists `DB_MACHINE_ID_PROVIDER_LOCK` (\n" +
                "\t`id` int unsigned not null,\n" +
                "\t`locked` BIT(1) NOT NULL DEFAULT b'0' comment '是否被锁住',\n" +
                "\t`lock_time` BIGINT(20) UNSIGNED NOT NULL COMMENT '加锁时间 单位毫秒',\n" +
                "\t`modified_on` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',\n" +
                "\tprimary key (`id`)\n" +
                ")\n" +
                "COMMENT='表锁记录'\n" +
                "COLLATE='utf8_general_ci'\n" +
                "ENGINE=InnoDB\n" +
                ";");
    }

    private MachineIdEntity getMachineIdEntity(Long id) {
        try {
            return jdbcTemplate.queryForObject("select id,type,modified_on,user_timestamp,last_timestamp,ip from DB_MACHINE_ID_PROVIDER where ID = ?", new Object[]{id}, new RowMapper<MachineIdEntity>() {
                @Override
                public MachineIdEntity mapRow(ResultSet resultSet, int i) throws SQLException {
                    MachineIdEntity machineIdEntity = new MachineIdEntity(resultSet.getLong("id"));
                    machineIdEntity.setLastTimestamp(resultSet.getLong("last_timestamp"));
                    machineIdEntity.setType(resultSet.getInt("type"));
                    machineIdEntity.setModifiedOn(resultSet.getTimestamp("modified_on").getTime());
                    machineIdEntity.setUserTimestamp(resultSet.getLong("user_timestamp"));
                    return machineIdEntity;
                }
            });
        } catch (EmptyResultDataAccessException e) {
            return null;
        }
    }

    private void insertMachineIdEntity(Long id, Integer type) {
        jdbcTemplate.update("insert into  DB_MACHINE_ID_PROVIDER (id,type,last_timestamp,modified_on,user_timestamp,ip) values (?,?,?,?,?,?)",
                new Object[]{id, type, 0L, getDataBaseTime(), System.currentTimeMillis(), IpUtils.getHostIp()});
    }

    private void updateMachineIdEntity(Long id, Integer type) {
        if (type == null) {
            jdbcTemplate.update("update DB_MACHINE_ID_PROVIDER set last_timestamp = ? ,modified_on = ?,user_timestamp = ? where id = ?", new Object[]{0L, getDataBaseTime(), System.currentTimeMillis(), id});
        } else {
            jdbcTemplate.update("update DB_MACHINE_ID_PROVIDER set last_timestamp = ? ,modified_on = ?,user_timestamp = ?,type= ? where id = ?", new Object[]{0L, getDataBaseTime(), System.currentTimeMillis(), type, id});
        }
    }

    private boolean lock() {
        int result = jdbcTemplate.update("update DB_MACHINE_ID_PROVIDER_LOCK set locked = 1, lock_time = ?,modified_on = ? where id = 1 and locked = 0 or unix_timestamp(modified_on)+lock_time < UNIX_TIMESTAMP(NOW())", new Object[]{LOCK_EXPIRE, getDataBaseTime()});
        if (result == 1) {
            return true;
        }
        Integer count = jdbcTemplate.queryForObject("select count(*) from DB_MACHINE_ID_PROVIDER_LOCK where id = 1", Integer.class);
        if (count == 0) {
            try {
                jdbcTemplate.update("insert into DB_MACHINE_ID_PROVIDER_LOCK (id,locked,lock_time) values (?,?,?)", new Object[]{1, 0, 0});
            } catch (Exception e) {
                //被创建过了
                lock();
            }
        }
        int failCount = 0;
        while (failCount < 5) {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            result = jdbcTemplate.update("update DB_MACHINE_ID_PROVIDER_LOCK set locked = 1, lock_time = ?,modified_on = ? where id = 1 and locked = 0 or unix_timestamp(modified_on)+lock_time < UNIX_TIMESTAMP(NOW())", new Object[]{LOCK_EXPIRE, getDataBaseTime()});
            if (result == 1)
                return true;
            failCount++;
        }
        return false;
    }

    private void unlock() {
        jdbcTemplate.update("update DB_MACHINE_ID_PROVIDER_LOCK set locked = 0, lock_time= 0 where id = 1");
    }
}
